feat(spec): register every error code that ships in dist — nine door: 'none' codes enter ERROR_CODE_LEDGER under the #16404 ruling (#16449) - #16652
Conversation
…'none' codes enter ERROR_CODE_LEDGER Under the #16404 ruling (option D) the ledger is the published face: every code shipped in dist is registered, door or no door. OBJECT_OWNERSHIP_CONFLICT (objectql), the seven STACK_* defineStack refusals and PLUGIN_UI_REQUIRED_KEY_MISSING (spec) gain rows; their boot-refusal classification rows ratchet out of dispatcher-error-vocabulary.ts, and check:dispatcher-error-vocabulary now refuses any verdict but foreign-vocabulary / runtime-pinned under packages/spec/src (the pin that keeps the class closed). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf
…for the nine new codes Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf
…oot-refusal rows; state the artifact reading in the changeset Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf
📓 Docs Drift CheckThis PR changes 2 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 135 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e517c2b0f4946e07b1b9bec99054ae260a4cdfc5 && git checkout e517c2b0f4946e07b1b9bec99054ae260a4cdfc5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cc238db8ba1755a40181a80a9d843835d69d8cdb db0ad84790f2ce94d62b3858c9f3572a54e22fe5 && git checkout -B drift-repro cc238db8ba1755a40181a80a9d843835d69d8cdb && git merge --no-ff db0ad84790f2ce94d62b3858c9f3572a54e22fe5
node scripts/docs-audit/affected-docs.mjs --json cc238db8ba1755a40181a80a9d843835d69d8cdb
|
…gister-shipped-error-codes
…ed tree Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf
Fixes #16449
Clause-②: yes — on the #16404 ruling's own authority (「registering a code widens the published face and is therefore
Clause-②: yes, door or no door」); this PR carriesneeds:contract-review.What this does
Under the #16404 ruling (director seat, decision batch #62, 2026-09-07, option D; maintainer 「同意」) the published contract face for error codes is
ERROR_CODE_LEDGER/StandardErrorCode, and everycodethat ships in a package'sdistis registered there, door or no door. Nine codes were shipping unregistered on this base and now have ledger rows, each under the package that stamps it:@objectstack/objectql:OBJECT_OWNERSHIP_CONFLICT(ADR-0029 D3,SchemaRegistry.registerObject, status 422)@objectstack/spec: the sevendefineStackrefusalsSTACK_SCHEMA_INVALID·STACK_CAPABILITY_UNKNOWN·STACK_CROSS_REFERENCE_INVALID·STACK_NAMESPACE_PREFIX_INVALID·STACK_SINGLE_APP_VIOLATION·STACK_HIERARCHY_SCOPE_CAPABILITY_REQUIRED·STACK_TRIGGER_CAPABILITY_REQUIRED(all status 422, finding(spec):defineStack's cross-reference refusals are bareErrors — no ADR-0112code/status— so five REFUSED item classes in the ADR-0130 matrix are distinguishable only by message text #14552 / finding(spec): six of defineStack's seven refusals are still bare Errors — no ADR-0112 code/status — leaving one envelope among neighbours that have none #15963), andPLUGIN_UI_REQUIRED_KEY_MISSING(spec:PluginSchemamakesstaticPath/slugreally required fortype: "ui"(superRefine), and core'sPlugininterface derives fromPluginDefinition(spec half of #16049) #16334, stamped on the zod issue'sparams.code; ridesPLUGIN_CONTRACT_VIOLATION's envelope)Their nine
boot-refusalclassification rows inpackages/runtime/src/dispatcher-error-vocabulary.tsratchet out (a registered code is skipped by the scan, so the rows would bestale-rowfindings); the reachability reading each row recorded now lives on its ledger row. The ledger header records the ruling as the "Door or no door" rule in its own words, and theboot-refusalverdict doc now says a row carrying it is a registration owed, not an exemption.The
door: 'none'shape — one shape, no second list (ZONE 3)The ledger already distinguishes rows only by the string and its comment; two door-less codes were already registered that way (
SETTINGS_ENGINE_NOT_BOUND,METADATA_SCHEMA_INVALID). So no new key and no second list: adoor: 'none'code is a string under the stamping package plus a comment stating itsstatusand the reachability reading. What the ledger lacked was the doctrine — its own "Retiring a code" paragraph cited a boot refusal (MONGODB_MULTI_TENANT_UNSUPPORTED, #8035) as grounds for NOT registering, which the ruling supersedes; that paragraph now says so, and the retirement ground is narrowed to the producerless class only.Measured unregistered population (A2.1) — on
origin/mainat8341ed21cInstrument: hits of
'CODE'inside theStandardErrorCode = z.enum([...])block plus hits inside theERROR_CODE_LEDGER = {...}block (the card's own instrument, anchored on the two declaration blocks rather than whole files; the card's whole-file controls read 2, this instrument's read 1 — either way a non-zero control beside the zeros).VALIDATION_ERROR(control)PERMISSION_DENIED(control)NAMESPACE_CONFLICT@objectstack/objectqlrow, #14748, with a door-driving pin (packages/runtime/src/package-door-namespace-conflict-code.test.ts)OBJECT_OWNERSHIP_CONFLICTSTACK_CROSS_REFERENCE_INVALIDSTACK_SCHEMA_INVALID+ 5STACK_*siblingsPLUGIN_UI_REQUIRED_KEY_MISSING(#16334, landed after the card's measurement)So the card's "nine" was eight unregistered plus one already registered; PR #16342 (#15963) landed at
1ecee3e53WITHOUT registering its six (its diff touched only the vocabulary,stack.zod.ts, its test and its changeset), contrary to the ruling's "register in that PR or land after #16449" — this PR closes that.PLUGIN_UI_REQUIRED_KEY_MISSINGis the ninth actually registered: it surfaced from the general pin (apackages/spec/srcstamp site the vocabulary classifiedboot-refusal), it is the same defect class, the fix is one mechanical row under the same gate family, andplugin.zod.tsis held by no other claim — the bounded in-place-fix exemption, declared here because the claim's file surface did not name that file (only its docstring moved). The genuinely-excluded set underpackages/spec/srcis the fiveforeign-vocabularysites (ERR_BULK_PER_ROW_HOOK_LIMIT,OS_METADATA_CONVERSION_CONFLICT,OS_METADATA_CONVERTED,SQLITE_ERROR×2) — a driver errno, conversion outcomes and a conformance fixture, not ADR-0112 codes; no allowlist was added.A2.2 — the doored codes, re-measured
NAMESPACE_CONFLICT: already registered (spec: registerNAMESPACE_CONFLICTinERROR_CODE_LEDGER— the ADR-0048 install-time namespace refusal now reaches a wire unregistered (the spec half of #14474) #14748); the wire already carrieserror.code: NAMESPACE_CONFLICTatPOST /api/v1/packages. Nothing changes here.OBJECT_OWNERSHIP_CONFLICT: the card says "has a door (demoted todeclaredCodetoday)"; the vocabulary's row (objectql: 11 error classes still spell their code as an inline literal, so a consumer cannot follow theby code, not instanceofconvention the docs already teach #16159) recordsdoor: 'none'with a measured argument, re-checked on this base — the only two non-testregisterObjectcallers outsideobjectqlare inpackages/metadata-protocol/src/protocol.ts(applyRegistryWriteThrough, which catches andconsole.warns, andloadMetaFromDb, which counts the row inerrors), and the two HTTP install sites callinstallPackage, which never callsregisterObject. No door on this tree; registration changes no HTTP body today.dispatcher-error-vocabulary.ts:642anchor did not survive: on this base line 642 sits inside theAMBIGUOUS_METADATA_STEMpending-registrationrow; theNAMESPACE_CONFLICTpending row was ratcheted out by spec: registerNAMESPACE_CONFLICTinERROR_CODE_LEDGER— the ADR-0048 install-time namespace refusal now reaches a wire unregistered (the spec half of #14474) #14748.Wire consequence (stated in the changeset,
@objectstack/specminor)For a code that reaches an HTTP door, registration changes what a client reads —
error.codebecomes the specific code instead of the status-derived member (VALIDATION_ERRORfor 422) with the spelling demoted todeclaredCode. Measured on this tree, none of the nine has such a door, so no HTTP body changes with this release; what changes is the face —ErrorCodegains nine members,REGISTERED_ERROR_CODESlists them, the generated docs references carry them (content/docs/references/api/error-code-ledger.mdx,contract.mdx;check:generatedfound nothing else stale), and each refusal'se.codeis a member of the union a consumer's exhaustiveswitchis written over. Should a door ever answer with one of these, the wire carries the specific code from then on; thedeclaredCodedemotion stays for genuinely unknown / third-party spellings. NoBREAKINGbanner: nothing is removed or renamed, every existing body parses as before; the only consumer-visible cost is type-level (nine more cases in an exhaustive switch over theErrorCodetype).@objectstack/runtimegets no changeset: its only source change is the classification table, whichtsupnever reaches (entrysrc/index.ts; nosrcfile importsdispatcher-error-vocabulary), so nothing published moves.The pin that keeps the class closed (A2.3)
scripts/check-dispatcher-error-vocabulary.mjs(requiredLint & Repo Gates) now refuses to classify apackages/spec/src/**stamp site as anything butforeign-vocabularyorruntime-pinned— aboot-refusal/pending-registration/sandbox-authoredrow for a spec site is aspec-face-unregisteredfinding naming the ruling and the remedy. It rides the gate's existing scanner (all ten stamp shapes,packages/**non-test source), so it is general over everycode:literal raised under the spec tree rather than a list of nine names; its--self-testgains a[#16449]battery of 7 cases (boot-refusal and pending-registration rows red; foreign-vocabulary admitted; the same row outsidepackages/spec/srcnot flagged — the control; a runtime-pinned template admitted; the finding text names #16404 and the ledger file; a registered code derives no site). Beside it, runtime pins by value:stack-refusal-envelopes.test.tsasserts everydefineStackrefusal's code parses againstErrorCodeand is under@objectstack/spec;plugin-ui-required-keys.test.tssection D does the same for its code;registry-ownership-refusal-envelope.test.tsassertsOBJECT_OWNERSHIP_CONFLICTparses and that 422 cannot have derived it;error-code-ledger.test.tsaccepts the batch under its owning packages withstandardSynonymOfundefined for each.Reverse verification — direction predicted before running: RED
Both legs run from the committed state, mutation proven on disk, restored by
git checkout HEAD -- pathand verified bygit diff HEADempty plusgit hash-objectequal to the HEAD blob (b671a497…ledger,0bf8fdb6…vocabulary):'STACK_SCHEMA_INVALID'ledger row (grep count 1 → 0) and re-add its formerboot-refusalvocabulary row (0 → 1, marker 1) →node scripts/check-dispatcher-error-vocabulary.mjsexit 1, exactly 1 finding, kindspec-face-unregistered, namingpackages/spec/src/stack.zod.ts/STACK_SCHEMA_INVALID/boot-refusal. Restored: rows 1 / 0 / marker 0, hashes match.vitest run src/stack-refusal-envelopes.test.ts src/api/error-code-ledger.test.tsexit 1 with exactly the two spec: register every error code that ships indist— the nine unregistereddefineStack/ ownership / namespace codes enterERROR_CODE_LEDGER/StandardErrorCode(the ledger is the published face, per the #16404 ruling) #16449 cases failing (every code is a member of the closed ErrorCode union…andaccepts the #16449 batch…), everything else green. Restored, hash matches.No build is involved in either leg: the gate reads spec SOURCE (
parseLedgerCodesanchors on the declaration) and the spec tests import the ledger source relatively, so the dist-ablation preflight does not apply.Verification (implementation head
c35fbe58f1; exit codes captured before any pipe)pnpm --filter @objectstack/spec buildunderos-verify-lock.sh→ build exit 0 (held 148s)pnpm --filter @objectstack/spec check:generated→ 1 of 15 stale (content/docs/references/**),--fixregenerated only that; rerun onc35fbe58f1: exit 0pnpm --filter @objectstack/spec typecheck→ 0;vitest runover 7 spec files (error-code-ledger,stack-refusal-envelopes,plugin-ui-required-keys,error-catalog-docs,contract,stack-cross-reference-envelope,type-alias-convention.pin) → 7 files / 147 tests passedvitest run --project localoverregistry-ownership-refusal-envelope,registry-conflict-code-constants,registry-namespace-install-gate→ 3 files / 20 tests passed;pnpm --filter @objectstack/objectql typecheck→ 0check:dispatcher-error-vocabulary(--self-test: 10 shapes + 329 assertions OK; run: 65 sites all classified, 264 ledger + 50 standard, spec face 5 sites) → 0;check:error-code-provenance→ 0 (319 stamp sites, 303 listed, 16 waived);check:error-code-casing→ 0;check:error-status-conformance→ 0;check:nul-bytes→ 0node scripts/pm/dispatch-gates.mjs --commandson the real diff, reconciled with--ran: 123 of 123 families accounted for); exit codes in the report comment on spec: register every error code that ships indist— the nine unregistereddefineStack/ ownership / namespace codes enterERROR_CODE_LEDGER/StandardErrorCode(the ledger is the published face, per the #16404 ruling) #16449. NOT MEASURED locally, by prerequisite not by finding:check:dual-build-cjs-loads(exit 3, needs every package'sdist),check:type-check-debt(exit 3, its re-measure prerequisite),check:pm-dispatch-gates(exit 124 twice — its own self-test exceeded 270s and 540s on the shared box).check:engine-split-ratio --days 90first refused on the shallow clone (exit 2), deepened withgit fetch --shallow-since=2026-06-02, rerun → 0.pnpm lintnarrowing, declared:eslint --no-inline-config --format jsonover the 9 changed files in the config's population (**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}minusNEVER_LINTED; the changed.md/.mdxare outside it) → exit 0, 9 files linted, 0 errors, 0 warnings; invariance:eslint.config.mjsline 328 declares noparserOptions.projectand no typed rules, so this diff cannot move any untouched file's verdict. The repo-wide run is CI's.error-envelope.conformance.test.ts) checks per-row invariants over the remaining rows and iterates the dispatcher-reachable set, which this diff does not touch (every removed row wasdoor: 'none'); the 31-package runtime closure was not built locally. CI runsTest Core.Post-merge re-verification (head
db0ad84790)origin/mainmoved to0a61db1f5dwhile this PR was open; the predecessor #16618 registered a code in the same ledger and regenerated the samecontent/docs/references/api/contract.mdxline (the enum member count), which a driver-lessmerge-treeprobe showed as a content conflict. Merged throughscripts/pm/os-regen-merge.sh(merge commit8800d90641, ⛔ no rebase, no force-push), then the two routed docs artifacts regenerated on the merged source and committed (db0ad84790). Re-taken on that head:check:dispatcher-error-vocabulary(self-test 329 assertions OK; run 65 sites all classified, spec face 5) → 0;check:error-code-provenance,check:error-code-casing,check:error-status-conformance,check:nul-bytes, speccheck:generated→ 0;pnpm --filter @objectstack/spec typecheck→ 0; the 7 spec test files → 147 passed; the 3 objectql test files → 20 passed; eslint over the same 9 files → 0 errors / 0 warnings; the gate family re-derived on the merged tree (124, one newly named familycheck:pm-widening-tells→ 0) reconciled 124 of 124. Driver-less probe on the merged head: clean (exit 0); the same probe on the pre-merge head against the same main still conflicts (exit 1) — the control that the probe discriminates. Main added nocode:stamp underpackages/spec/src(its only spec-srccodementions are lowercase zod issue-code comparisons, outside the gate's grammar).验收备注
door: 'none'(boot-refusal) codes that ship indist— the rest of the #16404 class after #16449 #16649: the 14 remainingboot-refusalrows (core,objectql,runtime,driver-memory,driver-mongodb,plugins/organizations) are the same class under the ruling; the card also names widening the spec-face rule to every published package once they are registered..changeset/stack-refusal-envelopes.md(fix(spec): every defineStack refusal carries an ADR-0112 envelope — six STACK_* codes beside STACK_CROSS_REFERENCE_INVALID #16342, pending release) still says "None of the six is registered inERROR_CODE_LEDGER"; both changesets ship in the same release. 承接者: the maintainer compiling release notes centrally (AGENTS.md,content/docs/releases/), not a code PR.needs:contract-reviewprocess sentences — is rendered wholesale intocontent/docs/references/api/error-code-ledger.mdxbygen:docs. 承接者: none identified (build-docs.tsowner; observation only).stack.zod.ts's docstring count "14 rows on the tree this landed against" for theboot-refusalclass is now historical (14 remain after this PR by coincidence of a different composition). 承接者: none.check:pm-dispatch-gatesself-test needs more than nine minutes on a loaded shared box. 承接者: the pm-dispatch lane (scripts/pm/).update_pull_request; the session-URL footer below was re-sent on a second edit. 承接者: pm-dispatchreferences/platform-readings.md.维护者速读(草稿)
改了什么:把九个已经随 npm 包发布、但没有登记在错误码总账(
ERROR_CODE_LEDGER)里的错误码正式登记:OBJECT_OWNERSHIP_CONFLICT(objectql)、七个defineStack的STACK_*拒绝码和PLUGIN_UI_REQUIRED_KEY_MISSING(spec)。同时给门禁加了一条规则:spec 包源码里抛出的任何错误码,要么在总账里,要么 CI 变红。为什么改:#16404 的裁决(决策批次 #62,选项 D)定了「总账就是对外契约面,只要随 dist 发布的码都必须登记,有没有 HTTP 出口都一样」。这九个码已经发布在外,下游
catch (e) { switch (e.code) }一旦依赖,就再也不能悄悄改名;没登记等于契约面上有个洞。卡里说的九个里,NAMESPACE_CONFLICT其实 #14748 已经登记过了;#16342 合并时没有按裁决登记它的六个码;PLUGIN_UI_REQUIRED_KEY_MISSING是卡之后新落地的同类,顺手一起补齐。风险与代价(含回滚):这一版不改任何 HTTP 响应体——九个码在当前代码里都没有 HTTP 出口(实测),所以线上行为不变;变的是类型层面:
ErrorCode联合类型多了九个成员,穷举switch的下游要多写九个分支(只会多不会少)。@objectstack/spec发 minor,不带 BREAKING。回滚就是 revert 这个 PR,不需要数据迁移。席位意见:(留空,由席位定稿)
你要做的:这是 Clause-② yes 的 PR(裁决自带),需要合同评审席位过一遍
needs:contract-review;确认「无 HTTP 出口 ⇒ 不改响应体」这个读数你认可;剩下 14 个同类未登记码在 #16649,按裁决排期即可。Generated by Claude Code